ποΈGitΠ―ΡΠ°ποΈ
Node / meshtastic / Meshtastic-Android / files / core / service / src / commonMain / kotlin / org / meshtastic / core / service / RadioControllerImpl.kt
Displaying Raw β’ Download
core/service/src/commonMain/kotlin/org/meshtastic/core/service/RadioControllerImpl.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 19.61 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.service
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.NonCancellable
Tff7b72import T7ee787kotlinx.coroutines.flow.StateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.collectLatest
Tff7b72import T7ee787kotlinx.coroutines.flow.combine
Tff7b72import T7ee787kotlinx.coroutines.flow.distinctUntilChanged
Tff7b72import T7ee787kotlinx.coroutines.flow.launchIn
Tff7b72import T7ee787kotlinx.coroutines.flow.onEach
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.sync.Mutex
Tff7b72import T7ee787kotlinx.coroutines.sync.withLock
Tff7b72import T7ee787kotlinx.coroutines.withContext
Tff7b72import T7ee787org.meshtastic.core.common.database.DatabaseManager
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionEpochs
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionLifecycle
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.repository.AdminController
Tff7b72import T7ee787org.meshtastic.core.repository.AdminEditScope
Tff7b72import T7ee787org.meshtastic.core.repository.CommandSender
Tff7b72import T7ee787org.meshtastic.core.repository.ConnectionIdentity
Tff7b72import T7ee787org.meshtastic.core.repository.MeshDataHandler
Tff7b72import T7ee787org.meshtastic.core.repository.MeshLocationManager
Tff7b72import T7ee787org.meshtastic.core.repository.MeshMessageProcessor
Tff7b72import T7ee787org.meshtastic.core.repository.MeshPrefs
Tff7b72import T7ee787org.meshtastic.core.repository.MessagingController
Tff7b72import T7ee787org.meshtastic.core.repository.NodeController
Tff7b72import T7ee787org.meshtastic.core.repository.NodeManager
Tff7b72import T7ee787org.meshtastic.core.repository.NodeRepository
Tff7b72import T7ee787org.meshtastic.core.repository.NotificationManager
Tff7b72import T7ee787org.meshtastic.core.repository.PacketRepository
Tff7b72import T7ee787org.meshtastic.core.repository.PlatformAnalytics
Tff7b72import T7ee787org.meshtastic.core.repository.QueryController
Tff7b72import T7ee787org.meshtastic.core.repository.RadioConfigRepository
Tff7b72import T7ee787org.meshtastic.core.repository.RadioController
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.RadioSessionContext
Tff7b72import T7ee787org.meshtastic.core.repository.ServiceRepository
Tff7b72import T7ee787org.meshtastic.core.repository.UiPrefs
Tff7b72import T7ee787org.meshtastic.proto.Channel
Tff7b72import T7ee787org.meshtastic.proto.ClientNotification
Tff7b72import T7ee787org.meshtastic.proto.Config
Tff7b72private Tff7b72data Tff7b72class T56d364AssociationSnapshotTb4b4b4(
Tff7b72val Te6edf3identityTb4b4b4: Te6edf3ConnectionIdentity?Tb4b4b4,
Tff7b72val Te6edf3selectedAddressTb4b4b4: Tffa657String?Tb4b4b4,
Tff7b72val Te6edf3sessionGenerationTb4b4b4: Tffa657LongTb4b4b4,
Tff7b72val Te6edf3activeSessionTb4b4b4: Te6edf3RadioSessionContext?Tb4b4b4,
Tb4b4b4)
Tff7b72internal Tff7b72suspend Tff7b72fun Td2a8ffrestoreLocalConfigurationIfOwnedTb4b4b4(
Te6edf3expectedDeviceAddressTb4b4b4: Tffa657String?Tb4b4b4,
Te6edf3currentDeviceAddressTb4b4b4: Tffa657String?Tb4b4b4,
Te6edf3configTb4b4b4: Te6edf3ConfigTb4b4b4,
Te6edf3primaryChannelTb4b4b4: Te6edf3Channel?Tb4b4b4,
Te6edf3editLocalSettingsTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3suspend Te6edf3AdminEditScopeTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3expectedDeviceAddress Tff7b72=Tff7b72= Tff7b72null Tff7b72|Tff7b72| Te6edf3currentDeviceAddress Tff7b72!Tff7b72= Te6edf3expectedDeviceAddressTb4b4b4) Tff7b72return Tff7b72false
Te6edf3editLocalSettings Tb4b4b4{
Te6edf3primaryChannelTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3setChannelTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Te6edf3setConfigTb4b4b4(Te6edf3configTb4b4b4)
Tb4b4b4}
Tff7b72return Tff7b72true
Tb4b4b4}
T8b949e/**
* Platform-agnostic [RadioController] composition root for any target where the service runs in-process (Desktop, iOS,
* or Android in single-process mode).
*
* Rather than implementing every command itself, this class **assembles** four focused collaborators β one per
* sub-interface β and delegates to them via Kotlin interface delegation, mirroring the SDK's layered API design
* ([AdminController] β `AdminApi`, [MessagingController] β `RadioClient.send*`, [NodeController]/[QueryController] β
* `AdminApi`/`TelemetryApi`/`RoutingApi`). When the SDK is adopted, each collaborator becomes a thin adapter and this
* class is the seam where they are wired together.
*
* Only the cross-cutting concerns that don't belong to any single sub-interface live here directly: connection-state
* surfacing, packet-id generation, location provisioning, and device-address switching.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffLongParameterListTa5d6ff"Tb4b4b4)
Tff7b72class T56d364RadioControllerImplTb4b4b4(
Tff7b72private Tff7b72val Te6edf3serviceRepositoryTb4b4b4: Te6edf3ServiceRepositoryTb4b4b4,
Te6edf3nodeRepositoryTb4b4b4: Te6edf3NodeRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3commandSenderTb4b4b4: Te6edf3CommandSenderTb4b4b4,
Tff7b72private Tff7b72val Te6edf3nodeManagerTb4b4b4: Te6edf3NodeManagerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3radioInterfaceServiceTb4b4b4: Te6edf3RadioInterfaceServiceTb4b4b4,
Tff7b72private Tff7b72val Te6edf3locationManagerTb4b4b4: Te6edf3MeshLocationManagerTb4b4b4,
Te6edf3packetRepositoryTb4b4b4: Te6edf3LazyTff7b72<Te6edf3PacketRepositoryTff7b72>Tb4b4b4,
Te6edf3dataHandlerTb4b4b4: Te6edf3LazyTff7b72<Te6edf3MeshDataHandlerTff7b72>Tb4b4b4,
Te6edf3analyticsTb4b4b4: Te6edf3PlatformAnalyticsTb4b4b4,
Tff7b72private Tff7b72val Te6edf3meshPrefsTb4b4b4: Te6edf3MeshPrefsTb4b4b4,
Te6edf3uiPrefsTb4b4b4: Te6edf3UiPrefsTb4b4b4,
Tff7b72private Tff7b72val Te6edf3databaseManagerTb4b4b4: Te6edf3DatabaseManagerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3notificationManagerTb4b4b4: Te6edf3NotificationManagerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3messageProcessorTb4b4b4: Te6edf3LazyTff7b72<Te6edf3MeshMessageProcessorTff7b72>Tb4b4b4,
Te6edf3radioConfigRepositoryTb4b4b4: Te6edf3RadioConfigRepositoryTb4b4b4,
Te6edf3scopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4,
Tff7b72private Tff7b72val Te6edf3onDeviceAddressChangedTb4b4b4: Tb4b4b4(Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tff7b72? Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3RadioControllerTb4b4b4,
Te6edf3AdminController Tff7b72by Te6edf3AdminControllerImplTb4b4b4(
Te6edf3commandSender Tff7b72= Te6edf3commandSenderTb4b4b4,
Te6edf3nodeManager Tff7b72= Te6edf3nodeManagerTb4b4b4,
Te6edf3radioConfigRepository Tff7b72= Te6edf3radioConfigRepositoryTb4b4b4,
Te6edf3connectionStateProvider Tff7b72= Te6edf3serviceRepositoryTb4b4b4,
Te6edf3scope Tff7b72= Te6edf3scopeTb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3MessagingController Tff7b72by Te6edf3MessagingControllerImplTb4b4b4(
Te6edf3commandSenderTb4b4b4,
Te6edf3nodeManagerTb4b4b4,
Te6edf3nodeRepositoryTb4b4b4,
Te6edf3dataHandlerTb4b4b4,
Te6edf3analyticsTb4b4b4,
Te6edf3packetRepositoryTb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3NodeController Tff7b72by Te6edf3NodeControllerImplTb4b4b4(Te6edf3commandSenderTb4b4b4, Te6edf3nodeManagerTb4b4b4, Te6edf3packetRepositoryTb4b4b4, Te6edf3scopeTb4b4b4)Tb4b4b4,
Te6edf3QueryController Tff7b72by Te6edf3QueryControllerImplTb4b4b4(Te6edf3commandSenderTb4b4b4, Te6edf3nodeManagerTb4b4b4, Te6edf3uiPrefsTb4b4b4) Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3deviceSwitchMutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)
Tff7b72override Tff7b72val Te6edf3sessionGenerationTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657LongTff7b72> Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3sessionGeneration
Tff7b72init Tb4b4b4{
T8b949e// Reconcile the connection-session identity at every transport-session boundary (stop/start cycle), not only
T8b949e// when the selected address changes. Without this, a same-address same-device reconnect can retain the old
T8b949e// ConnectionIdentity. Capture the initial generation before subscribing instead of blindly dropping the first
T8b949e// emission: if a transport starts between capture and collector subscription, the collector's first replay is
T8b949e// the real boundary. The generation-aware atomic clear removes the old identity without erasing a fresh one
T8b949e// that the new session may already have published while this collector was awaiting dispatch.
Tff7b72val Te6edf3initialSessionGeneration Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3sessionGenerationTb4b4b4.Te6edf3value
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3sessionGeneration
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3generation Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3generation Tff7b72=Tff7b72= Te6edf3initialSessionGenerationTb4b4b4) Tff7b72returnTf0883e@onEach
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[DeviceAssociation] reconciling identity at session-generation boundary gen=Tffd700$Te6edf3generationTa5d6ff" Tb4b4b4}
Te6edf3nodeManagerTb4b4b4.Te6edf3clearStaleConnectionIdentityTb4b4b4(Te6edf3generationTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Te6edf3scopeTb4b4b4)
T8b949e// Unify per-device databases across transports. When a fresh connection-handshake identity
T8b949e// arrives (nodeNum + deviceId from the same MyNodeInfo), tell the DatabaseManager to claim
T8b949e// (or merge into) that device's canonical DB, so the same device reached over BLE, TCP, or
T8b949e// USB shares one stored history. The hardware device id (when reported) is the durable claim
T8b949e// key β firmware 2.8 renumbers devices (num = crc32(public_key)) on upgrade/erase/re-key β
T8b949e// with the node number as fallback. Keyed on (address, identity) so a second transport for
T8b949e// the same device re-fires even though the identity itself is unchanged.
T8b949e//
T8b949e// Uses the dedicated connectionIdentity source (not the general myNodeNum/myDeviceId flows)
T8b949e// so that a stale cached identity from a previous transport cannot associate the new address
T8b949e// with the old node number. Each identity carries the address selected for its handshake, so correctness does
T8b949e// not depend on delivery order between independent StateFlows. The selected address is checked again at the
T8b949e// point of association to discard a session that became stale while queued for collection.
T8b949e//
T8b949e// Collect the nullable identity together with both active-session coordinates. The admitted lifecycle lease
T8b949e// closes new work on teardown but delays session rollover until association and any destination transaction
T8b949e// commit return. collectLatest still cancels work whose identity or selected address changes independently.
T8b949e// CancellationException preserves structured cancellation; recoverable Exceptions are logged, and fatal
T8b949e// Errors propagate.
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Te6edf3scopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3combineTb4b4b4(
Te6edf3nodeManagerTb4b4b4.Te6edf3connectionIdentityTb4b4b4,
Te6edf3meshPrefsTb4b4b4.Te6edf3deviceAddressTb4b4b4,
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3sessionGenerationTb4b4b4,
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3activeSessionTb4b4b4,
Tb4b4b4) Tb4b4b4{ Te6edf3identityTb4b4b4, Te6edf3selectedAddressTb4b4b4, Te6edf3sessionGenerationTb4b4b4, Te6edf3activeSession Tff7b72-Tff7b72>
Te6edf3AssociationSnapshotTb4b4b4(Te6edf3identityTb4b4b4, Te6edf3selectedAddressTb4b4b4, Te6edf3sessionGenerationTb4b4b4, Te6edf3activeSessionTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3collectLatest Tb4b4b4{ Te6edf3snapshot Tff7b72-Tff7b72>
Tff7b72val Te6edf3identity Tff7b72= Te6edf3snapshotTb4b4b4.Te6edf3identity Tff7b72?: Tff7b72returnTf0883e@collectLatest
Tff7b72val Te6edf3authorizingSession Tff7b72=
Te6edf3RadioSessionContextTb4b4b4(Te6edf3generation Tff7b72= Te6edf3identityTb4b4b4.Te6edf3sessionGenerationTb4b4b4, Te6edf3address Tff7b72= Te6edf3identityTb4b4b4.Te6edf3addressTb4b4b4)
Tff7b72if Tb4b4b4(
Te6edf3snapshotTb4b4b4.Te6edf3selectedAddress Tff7b72!Tff7b72= Te6edf3identityTb4b4b4.Te6edf3address Tff7b72|Tff7b72|
Te6edf3snapshotTb4b4b4.Te6edf3sessionGeneration Tff7b72!Tff7b72= Te6edf3identityTb4b4b4.Te6edf3sessionGeneration Tff7b72|Tff7b72|
Te6edf3snapshotTb4b4b4.Te6edf3activeSession Tff7b72!Tff7b72= Te6edf3authorizingSession
Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[DeviceAssociation] skip stale-session node=Tffd700${Te6edf3identityTb4b4b4.Te6edf3nodeNumTffd700}Ta5d6ff" Tb4b4b4}
Tff7b72returnTf0883e@collectLatest
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{
Ta5d6ff"Ta5d6ff[DeviceAssociation] associate address=... node=Tffd700${Te6edf3identityTb4b4b4.Te6edf3nodeNumTffd700}Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff deviceIdPresent=Tffd700${Te6edf3identityTb4b4b4.Te6edf3deviceId Tff7b72!Tff7b72= Tff7b72nullTffd700}Ta5d6ff"
Tb4b4b4}
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3admitted Tff7b72=
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3runWithSessionLeaseTb4b4b4(Te6edf3authorizingSessionTb4b4b4) Tb4b4b4{ Te6edf3lease Tff7b72-Tff7b72>
Te6edf3databaseManagerTb4b4b4.Te6edf3associateDeviceTb4b4b4(
Te6edf3identityTb4b4b4.Te6edf3addressTb4b4b4,
Te6edf3identityTb4b4b4.Te6edf3nodeNumTb4b4b4,
Te6edf3identityTb4b4b4.Te6edf3deviceIdTb4b4b4,
Te6edf3leaseTff7b72::Te6edf3isCurrentTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3admittedTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[DeviceAssociation] skip revoked-session node=Tffd700${Te6edf3identityTb4b4b4.Te6edf3nodeNumTffd700}Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3cancellationTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3cancellation
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3failureTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3failureTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDevice association failed for current radio sessionTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e// ββ Connection State ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tff7b72override Tff7b72val Te6edf3connectionLifecycleTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ConnectionLifecycleTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionLifecycle
Tff7b72override Tff7b72val Te6edf3connectionStateTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ConnectionStateTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionState
Tff7b72override Tff7b72val Te6edf3connectionEpochsTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ConnectionEpochsTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionEpochs
Tff7b72override Tff7b72val Te6edf3clientNotificationTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ClientNotification?Tff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3serviceRepositoryTb4b4b4.Te6edf3clientNotification
Tff7b72override Tff7b72fun Td2a8ffclearClientNotificationTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3serviceRepositoryTb4b4b4.Te6edf3clearClientNotificationTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e// ββ Packet ID & Location ββββββββββββββββββββββββββββββββββββββββββββββββ
Tff7b72override Tff7b72fun Td2a8ffgeneratePacketIdTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Int Tff7b72= Te6edf3commandSenderTb4b4b4.Te6edf3generatePacketIdTb4b4b4(Tb4b4b4)
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffrestoreLocalConfigurationTb4b4b4(
Te6edf3expectedDeviceAddressTb4b4b4: Tffa657String?Tb4b4b4,
Te6edf3configTb4b4b4: Te6edf3ConfigTb4b4b4,
Te6edf3primaryChannelTb4b4b4: Te6edf3Channel?Tb4b4b4,
Tb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Te6edf3deviceSwitchMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Te6edf3restoreLocalConfigurationIfOwnedTb4b4b4(
Te6edf3expectedDeviceAddress Tff7b72= Te6edf3expectedDeviceAddressTb4b4b4,
Te6edf3currentDeviceAddress Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3config Tff7b72= Te6edf3configTb4b4b4,
Te6edf3primaryChannel Tff7b72= Te6edf3primaryChannelTb4b4b4,
Te6edf3editLocalSettings Tff7b72= Tb4b4b4{ Te6edf3block Tff7b72-Tff7b72> Te6edf3editLocalSettingsTb4b4b4(Te6edf3blockTb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffstartProvideLocationTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3locationManagerTb4b4b4.Te6edf3restartTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffstopProvideLocationTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3locationManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e// ββ Device Address ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffsetDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3deviceSwitchMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
T8b949e// The transport service updates its selected-address flow synchronously, while MeshPrefs persists on an
T8b949e// asynchronous DataStore scope. Prefer the transport snapshot so back-to-back selections cannot roll back
T8b949e// to a stale preference value; fall back to MeshPrefs during startup before the transport has a selection.
Tff7b72val Te6edf3transportAddress Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3previousAddress Tff7b72= Te6edf3transportAddress Tff7b72?: Te6edf3meshPrefsTb4b4b4.Te6edf3deviceAddressTb4b4b4.Te6edf3value
Tff7b72if Tb4b4b4(Te6edf3address Tff7b72=Tff7b72= Te6edf3previousAddressTb4b4b4) Tb4b4b4{
T8b949e// This is intentionally more than a preference no-op: when the selected transport is disconnected,
T8b949e// setDeviceAddress() cycles it as a repair attempt; when it is already connected, the transport
T8b949e// reports a no-op and the postcondition below confirms that the requested address remains selected.
Tff7b72val Te6edf3accepted Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4)
T8b949e// SharedRadioInterfaceService reports an already-connected no-op as false; confirm that postcondition
T8b949e// without allowing a genuine rejection to persist preferences or publish a success callback.
Te6edf3checkTb4b4b4(Te6edf3accepted Tff7b72|Tff7b72| Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= Te6edf3addressTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ffTransport rejected the existing device addressTa5d6ff"
Tb4b4b4}
Te6edf3meshPrefsTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
T8b949e// Revoke the old transport session before publishing a different database. Otherwise the old transport
T8b949e// can deliver one last frame after switchDevice() has moved currentDb, writing stale-session data into
T8b949e// the new device's database.
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3switchDeviceTb4b4b4(Te6edf3addressTb4b4b4)
Te6edf3checkTb4b4b4(Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ffTransport rejected the new device addressTa5d6ff"
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3failureTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{ Te6edf3rollbackDeviceSwitchTb4b4b4(Te6edf3previousAddressTb4b4b4, Te6edf3failureTb4b4b4) Tb4b4b4}
Tff7b72throw Te6edf3failure
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e// Keep callbacks outside the transition mutex so observers cannot deadlock by scheduling another selection.
Te6edf3onDeviceAddressChangedTff7b72?.Te6edf3invokeTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffrequestGattCacheInvalidationOnNextConnectTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3requestGattCacheInvalidationOnNextConnectTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffswitchDeviceTb4b4b4(Te6edf3deviceAddrTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDevice address changed, switching database and clearing node DBTa5d6ff" Tb4b4b4}
T8b949e// Revoke identity and persistence before publishing the new database. A node write already admitted against
T8b949e// the old pool drains through DatabaseManager; a queued write rechecks allowNodeDbWrites and cannot start on
T8b949e// the new pool with the old node snapshot.
Te6edf3nodeManagerTb4b4b4.Te6edf3clearConnectionIdentityTb4b4b4(Tb4b4b4)
Te6edf3nodeManagerTb4b4b4.Te6edf3setAllowNodeDbWritesTb4b4b4(Tff7b72falseTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[DeviceAssociation] session-cleared before address switchTa5d6ff" Tb4b4b4}
Te6edf3messageProcessorTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3clearEarlyPacketsTb4b4b4(Tb4b4b4)
Te6edf3databaseManagerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Te6edf3deviceAddrTb4b4b4)
Te6edf3nodeManagerTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)
Te6edf3notificationManagerTb4b4b4.Te6edf3cancelAllTb4b4b4(Tb4b4b4)
Te6edf3nodeManagerTb4b4b4.Te6edf3loadCachedNodeDBTb4b4b4(Tb4b4b4)
T8b949e// Commit the persisted selection last. MeshPrefs writes asynchronously, so the transport's synchronous
T8b949e// selected-address snapshot remains the rollback authority for a rapid subsequent selection.
Te6edf3meshPrefsTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3deviceAddrTb4b4b4)
Tb4b4b4}
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffrollbackDeviceSwitchTb4b4b4(Te6edf3previousAddressTb4b4b4: Tffa657String?Tb4b4b4, Te6edf3originalFailureTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Tff7b72suspend Tff7b72fun Td2a8ffattemptRollbackTb4b4b4(Te6edf3descriptionTb4b4b4: Tffa657StringTb4b4b4, Te6edf3blockTb4b4b4: Te6edf3suspend Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Tff7b72try Tb4b4b4{
Te6edf3blockTb4b4b4(Tb4b4b4)
Tff7b72true
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3rollbackFailureTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3originalFailureTb4b4b4.Te6edf3addSuppressedTb4b4b4(Te6edf3rollbackFailureTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3rollbackFailureTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to roll back Tffd700$Te6edf3descriptionTa5d6ff after device-switch failureTa5d6ff" Tb4b4b4}
Tff7b72false
Tb4b4b4}
T8b949e// Database ownership is the safety boundary. Never restore a transport until the database that belongs to it
T8b949e// is active again; otherwise old-radio frames can resume against the new device's pool.
Tff7b72val Te6edf3databaseRestored Tff7b72=
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6ffdatabase selectionTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3databaseManagerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Te6edf3previousAddressTb4b4b4) Tb4b4b4}
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3databaseRestoredTb4b4b4) Tb4b4b4{
T8b949e// Keep each cleanup independent: failure in one best-effort reset must not prevent transport deselection.
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fffail-closed node-write disableTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3setAllowNodeDbWritesTb4b4b4(Tff7b72falseTb4b4b4) Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fffail-closed connection-identity clearTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3clearConnectionIdentityTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fffail-closed node-state clearTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fffail-closed early-packet clearTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3clearEarlyPacketsTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fffail-closed notification clearTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3notificationManagerTb4b4b4.Te6edf3cancelAllTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fffail-closed persisted selectionTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3meshPrefsTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Tff7b72nullTb4b4b4) Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fffail-closed transport selectionTa5d6ff"Tb4b4b4) Tb4b4b4{
Te6edf3checkTb4b4b4(Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Tff7b72nullTb4b4b4)Tb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffTransport rejected fail-closed deselectionTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tff7b72return
Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6ffpersisted device selectionTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3meshPrefsTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3previousAddressTb4b4b4) Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6ffnode stateTa5d6ff"Tb4b4b4) Tb4b4b4{
Te6edf3nodeManagerTb4b4b4.Te6edf3clearConnectionIdentityTb4b4b4(Tb4b4b4)
Te6edf3nodeManagerTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)
Te6edf3messageProcessorTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3clearEarlyPacketsTb4b4b4(Tb4b4b4)
Te6edf3notificationManagerTb4b4b4.Te6edf3cancelAllTb4b4b4(Tb4b4b4)
Te6edf3nodeManagerTb4b4b4.Te6edf3loadCachedNodeDBTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3attemptRollbackTb4b4b4(Ta5d6ff"Ta5d6fftransport selectionTa5d6ff"Tb4b4b4) Tb4b4b4{
Te6edf3checkTb4b4b4(Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3previousAddressTb4b4b4)Tb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ffTransport rejected the previous device addressTa5d6ff"
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.14s